package asem;

import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;

import ppal.ErroresSemanticos;
import alex.TipoToken;
import alex.Token;
import asem.ast.sentencias.NodoBloque;
import asem.excepciones.ExcepcionAtributoConNombreDeMetodo;
import asem.excepciones.ExcepcionAtributoDuplicado;
import asem.excepciones.ExcepcionClaseDuplicada;
import asem.excepciones.ExcepcionConstructorDuplicado;
import asem.excepciones.ExcepcionConstructorInvalido;
import asem.excepciones.ExcepcionMainNoDeclarado;
import asem.excepciones.ExcepcionMetodoConNombreDeClase;
import asem.excepciones.ExcepcionMetodoDuplicado;
import asem.excepciones.ExcepcionParametroDuplicado;
import asem.excepciones.ExcepcionVariableDuplicada;
import asem.tablaSimbolos.AtributoDeInstancia;
import asem.tablaSimbolos.Clase;
import asem.tablaSimbolos.Constructor;
import asem.tablaSimbolos.Metodo;
import asem.tablaSimbolos.Parametro;
import asem.tablaSimbolos.VariableLocal;
import asem.tablaSimbolos.tipo.Tipo;
import asem.tablaSimbolos.tipo.TipoBoolean;
import asem.tablaSimbolos.tipo.TipoChar;
import asem.tablaSimbolos.tipo.TipoInt;
import asem.tablaSimbolos.tipo.TipoMetodo;
import asem.tablaSimbolos.tipo.TipoString;
import asem.tablaSimbolos.tipo.TipoVoid;

// Clase Singleton
public class TablaSimbolos {

	public static final TablaSimbolos tabla = new TablaSimbolos();

	private HashMap<String, Clase> clases;
	private Clase claseActual;
	private Metodo metodoActual;

	private TablaSimbolos() {
		clases = new HashMap<String, Clase>();
		metodoActual = null;
		claseActual = null;

		clasesPredefinidas();
	}

	public static TablaSimbolos getInstance() {
		return tabla;
	}

	public Clase claseActual() {
		return claseActual;
	}

	public Metodo metodoActual() {
		return metodoActual;
	}

	public void setMetodoActual(Metodo actual)
	{
		metodoActual = actual;
	}
	
	public Clase getClase(String string) {
		return clases.get(string);
	}

	public void crearClase(Token identificador) {
		if (clases.get(identificador.getLexema()) == null) {
			claseActual = new Clase(identificador);
		} else
			ErroresSemanticos.errores.addExcepcion(new ExcepcionClaseDuplicada(
					identificador));
		// throw new ExcepcionClaseExistente(identificador);
	}

	public void agregarAtributosDeInstancia(Tipo tipo,
			LinkedList<Token> conjuntoVariables) {
		for (Token i : conjuntoVariables) {
			// Si no existe variable con tal nombre declarada previamente
			if (claseActual.getAtributosDeInstancia().get(i.getLexema()) == null) {
				claseActual.getAtributosDeInstancia().put(
						i.getLexema(),
						new AtributoDeInstancia(i, tipo, claseActual
								.getNombre()));
			} else
				ErroresSemanticos.errores
						.addExcepcion(new ExcepcionAtributoDuplicado(i,
								claseActual.getNombre()));
			// throw new ExcepcionAtributoDeInstanciaExistente(i,
			// claseActual.getNombre());
		}
	}

	public void crearConstructor(Token constructor) {
		if (!constructor.getLexema().equals(claseActual.getNombre()))
			throw new ExcepcionConstructorInvalido(constructor,
					claseActual.getNombre());

		if (claseActual.getConstructor() == null) { // Si no tiene un
													// constructor
			Constructor ctor = new Constructor(constructor);
			ctor.setPerteneceA(claseActual.getNombre());
			ctor.setAST(new NodoBloque()); // Bloque vacio
			claseActual.setConstructor(ctor);
		} else
			ErroresSemanticos.errores
					.addExcepcion(new ExcepcionConstructorDuplicado(
							constructor, claseActual.getNombre()));
		// throw new ExcepcionConstructorExistente(constructor,
		// claseActual.getNombre());
	}

	public void crearMetodo(Token identificador, TipoToken modificador,
			TipoMetodo tipo) {

		if(identificador.getLexema().equals(claseActual.getNombre()))
			ErroresSemanticos.errores
			.addExcepcion(new ExcepcionMetodoConNombreDeClase(identificador.getLexema()));
		else
		{
			if (claseActual.getMetodos().get(identificador.getLexema()) == null)
				metodoActual = new Metodo(identificador, modificador, tipo,
						claseActual.getNombre());
			else
				ErroresSemanticos.errores
						.addExcepcion(new ExcepcionMetodoDuplicado(identificador,
								claseActual.getNombre()));
			// throw new ExcepcionMetodoExistente(identificador,
			// claseActual.getNombre());
		}
		
		
	}

	public void agregarArgFormal(boolean enConst, Tipo tipo, Token identificador) {
		Constructor aux;
		if (enConst)
			aux = claseActual.getConstructor();
		else
			aux = metodoActual;

		if (aux.getParametros().get(identificador.getLexema()) == null) {
			aux.incrementarCantParametros();
			int lugar = aux.getCantParametros();
			aux.getParametros().put(identificador.getLexema(),
					new Parametro(identificador, tipo, lugar));
		} else
			ErroresSemanticos.errores
					.addExcepcion(new ExcepcionParametroDuplicado(enConst,
							identificador, aux.getNombre(), claseActual
									.getNombre()));

		// throw new ExcepcionParametroExistente(enConst, identificador,
		// aux.getNombre(), claseActual.getNombre());
	}

	public void agregarVariablesLocales(boolean enConst, Tipo tipo,
			LinkedList<Token> identificadores) {
		Constructor aux;
		if (enConst)
			aux = claseActual.getConstructor();
		else
			aux = metodoActual;

		for (Token id : identificadores) {
			if (aux.getVariablesLocales().get(id.getLexema()) == null) {
				aux.getVariablesLocales().put(id.getLexema(),
						new VariableLocal(id, tipo, aux.getNombre()));
			} else
				ErroresSemanticos.errores
						.addExcepcion(new ExcepcionVariableDuplicada(enConst,
								id, aux.getNombre(), claseActual.getNombre()));
			// throw new ExcepcionVariableLocalExistente(enConst, id,
			// aux.getNombre(), claseActual.getNombre());
		}
	}

	public void finProcesamientoClase() {
		// Si no tiene constructor, se crea uno por defecto
		if (claseActual.getConstructor() == null)
			crearConstructor(claseActual.getIdentificador());

		// Si no se le ha declarado herencia, se la hace heredar de 'Object'
		if (claseActual.getHeredaDe() == null)
			claseActual.setHeredaDe("Object");

		clases.put(claseActual.getNombre(), claseActual);
		claseActual = null;
	}

	public void finProcesamientoMetodo() {
		claseActual.getMetodos().put(metodoActual.getNombre(), metodoActual);
		metodoActual = null;
	}

	public void agregarASTMetodo(NodoBloque ast) {
		metodoActual.setAST(ast);
	}

	public void agregarASTConstructor(NodoBloque ast) {
		claseActual.getConstructor().setAST(ast);
	}

	public HashMap<String, Clase> getClases() {
		return clases;
	}

	public void chequeoDeclaraciones() {
		for (Clase clase : clases.values()) {
			if (!clase.getNombre().equals("Object")
					&& !clase.getNombre().equals("System")) {
				claseActual = clase;
				clase.chequeoHerencia();
				clase.chequeoDeclaraciones();
				clase.chequeoSentencias();
			}
		}
	}

	public void chequearMetodoMain() {
		boolean hayMain = false;
		Iterator<Clase> it = clases.values().iterator();
		Clase actual = it.next();
		while (!hayMain && it.hasNext()) {
			hayMain = actual.tieneMain();
			actual = it.next();
		}

		if (!hayMain)
			ErroresSemanticos.errores
					.addExcepcion(new ExcepcionMainNoDeclarado());
		// throw new ExcepcionMainNoDeclarado();
	}

	private void clasesPredefinidas() {
		Clase object = new Clase(new Token(TipoToken.L_Identificador, "Object"));
		Constructor ctor = new Constructor(new Token(TipoToken.L_Identificador,
				"Object"));
		ctor.setPerteneceA(object.getNombre());
		object.setConstructor(ctor);
		clases.put("Object", object);
		// crearClaseSystem();
	}

	private void crearClaseSystem() {

		Clase system = new Clase(new Token(TipoToken.L_Identificador, "System"));
		Constructor ctor = new Constructor(new Token(TipoToken.L_Identificador,
				"System"));
		ctor.setPerteneceA(system.getNombre());
		system.setConstructor(ctor);

		agregarMetodoASystem(system, new Token(TipoToken.L_Identificador,
				"read"), new TipoInt());
		agregarMetodoASystem(system, new Token(TipoToken.L_Identificador,
				"printB"), new TipoVoid(), "b", new TipoBoolean());
		agregarMetodoASystem(system, new Token(TipoToken.L_Identificador,
				"printC"), new TipoVoid(), "c", new TipoChar());
		agregarMetodoASystem(system, new Token(TipoToken.L_Identificador,
				"printI"), new TipoVoid(), "i", new TipoInt());
		agregarMetodoASystem(system, new Token(TipoToken.L_Identificador,
				"printS"), new TipoVoid(), "s", new TipoString());
		agregarMetodoASystem(system, new Token(TipoToken.L_Identificador,
				"println"), new TipoVoid());
		agregarMetodoASystem(system, new Token(TipoToken.L_Identificador,
				"printBln"), new TipoVoid(), "b", new TipoBoolean());
		agregarMetodoASystem(system, new Token(TipoToken.L_Identificador,
				"printCln"), new TipoVoid(), "c", new TipoChar());
		agregarMetodoASystem(system, new Token(TipoToken.L_Identificador,
				"printIln"), new TipoVoid(), "i", new TipoInt());
		agregarMetodoASystem(system, new Token(TipoToken.L_Identificador,
				"printSln"), new TipoVoid(), "s", new TipoString());

		clases.put("System", system);
	}

	private void agregarMetodoASystem(Clase system, Token identificador,
			TipoMetodo tipo) {
		Metodo nuevo = new Metodo(identificador, TipoToken.C_static, tipo,
				"System");
		system.getMetodos().put(identificador.getLexema(), nuevo);
	}

	private void agregarMetodoASystem(Clase system, Token identificador,
			TipoMetodo tipo, String param, Tipo tipoParametro) {
		Metodo nuevo = new Metodo(identificador, TipoToken.C_static, tipo,
				"System");
		nuevo.getParametros().put(
				param,
				new Parametro(new Token(TipoToken.L_Identificador, "b"),
						tipoParametro, 1));
		system.getMetodos().put(identificador.getLexema(), nuevo);
	}

	public void nombresAtributosNoConflictivos()
	{
		for(AtributoDeInstancia at: claseActual.getAtributosDeInstancia().values())
		{
			if (claseActual.getMetodos().get(at.getNombre()) != null)
				ErroresSemanticos.errores
				.addExcepcion(new ExcepcionAtributoConNombreDeMetodo(at.getNombre()));
				//throw new ExcepcionAtributoConNombreDeMetodo(at.getNombre());
		}
	}
}
